# -*- coding: utf-8 -*-

import logging
import math
import datetime

from xml.dom import minidom

DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ'

def _to_cdata( str ):
	if not str:
		return '<![CDATA[]]>'
	return '<![CDATA[%s]]>' % str.replace( ']]>', '???' )

def _to_xml( tag, attributes = {}, content = None, cdata = None ):
	result = '\n<%s' % tag
	if attributes:
		for attribute in attributes.keys():
			result += ' %s="%s"' % ( attribute, attributes[ attribute ] )
	if content:
		if cdata:
			result += '>%s</%s>' % ( _to_cdata( content ), tag )
		else:
			result += '>%s</%s>' % ( content, tag )
	else:
		result += ' />'

	result += ''
	
	return result

def _parse_time( str ):
	if not str:
		return None
	return datetime.datetime.strptime( str, DATE_FORMAT )

def _to_number( str, default = 0 ):
	if not str:
		return None
	return float( str )

def _length( locations = [], _3d = None ):
	if not locations:
		return 0
	length = 0
	for i in range( len( locations ) ):
		if i > 0:
			previous_location = locations[ i - 1 ]
			location = locations[ i ]
			if _3d:
				d = location.distance_3d( previous_location )
			else:
				d = location.distance_2d( previous_location )
			if d != 0 and not d:
				logging.debug( 'dosli do null %s <-> %s _3d:%s' % ( location, previous_location, _3d ) )
				return None
			length += d
			#print 'length -> %s' % length
	return length

def _find_first_node( node, child_node_name ):
	if not node or not child_node_name:
		return None
	child_nodes = node.childNodes
	for child_node in child_nodes:
		if child_node.nodeName == child_node_name:
			return child_node
	return None

def length_3d( locations = [] ):
	return _length( locations, True )

def length_2d( locations = [] ):
	return _length( locations, None )

class Location:

	# One degree in meters:
	ONE_DEGREE = 1000. * 10000.8 / 90.

	latitude = None
	longitude = None
	elevation = None

	def __init__( self, latitude, longitude, elevation = None ):
		self.latitude = latitude
		self.longitude = longitude
		self.elevation = elevation

	def has_elevation( self ):
		# TODO ?
		return self.elevation or self.elevation == 0

	def distance_2d( self, location ):
		if not location:
			return None

		coef = math.cos( self.latitude / 180. * math.pi )
		x = self.latitude - location.latitude
		y = ( self.longitude - location.longitude ) * coef

		return math.sqrt( x * x + y * y ) * self.ONE_DEGREE

	def distance_3d( self, location ):
		distance = self.distance_2d( location )

		if distance == 0:
			return 0

		if not distance or not location.has_elevation() or not self.has_elevation():
			return None

		h = location.elevation - self.elevation

		return math.sqrt( distance * distance + h * h )

	def __str__( self ):
		return '[loc:%s,%s@%s]' % ( self.latitude, self.longitude, self.elevation )

class GPXWaypoint( Location ):

	time = None
	name = None
	description = None
	symbol = None
	type = None

	def __init__( self, latitude, longitude, elevation = None, time = None, name = None, description = None, symbol = None, type = None ):
		Location.__init__( self, latitude, longitude, elevation )

		self.time = time
		self.name = name
		self.description = description
		self.symbol = symbol
		self.type = type

	def __str__( self ):
		return '[wpt{%s}:%s,%s@%s]' % ( self.name, self.latitude, self.longitude, self.elevation )

	def to_xml( self ):
		content = _to_xml( 'ele', content = self.elevation )
		if self.time:
			content = _to_xml( 'time', content = self.time.strftime( DATE_FORMAT ) )
		content = _to_xml( 'name', content = self.name, cdata = True )
		content = _to_xml( 'desc', content = self.description, cdata = True )
		content = _to_xml( 'sym', content = self.symbol, cdata = True )
		content = _to_xml( 'type', content = self.type, cdata = True )

		return _to_xml( 'wpt', attributes = { 'lat': self.latitude, 'lon': self.longitude }, content = content )

class GPXRoute:

	name = None
	description = None
	number = None

	route_points = []

	def __init__( self, name = None, description = None, number = None ):
		self.name = name
		self.description = description
		self.number = number

		self.route_points = []

	def length( self ):
		return length_2d( route_points )

	def to_xml( self ):
		content = _to_xml( 'name', content = self.name, cdata = True )
		content += _to_xml( 'desc', content = self.description, cdata = True )
		content += _to_xml( 'number', content = self.number )
		for route_point in self.route_points:
			content += route_point.to_xml()

		return _to_xml( 'rte', content = content )

class GPXRoutePoint( Location ):

	time = None
	name = None
	description = None
	symbol = None
	type = None
	comment = None

	def __init__( self, latitude, longitude, elevation = None, time = None, name = None, description = None, symbol = None, type = None, comment = None ):
		Location.__init__( self, latitude, longitude, elevation )

		self.time = time
		self.name = name
		self.description = description
		self.symbol = symbol
		self.type = type
		self.comment = comment

	def __str__( self ):
		return '[rtept{%s}:%s,%s@%s]' % ( self.name, self.latitude, self.longitude, self.elevation )

	def to_xml( self ):
		content = _to_xml( 'ele', content = self.elevation )
		if self.time:
			content = _to_xml( 'time', content = self.time.strftime( DATE_FORMAT ) )
		content = _to_xml( 'name', content = self.name, cdata = True )
		content = _to_xml( 'cmt', content = self.comment, cdata = True )
		content = _to_xml( 'desc', content = self.description, cdata = True )
		content = _to_xml( 'sym', content = self.symbol, cdata = True )
		content = _to_xml( 'type', content = self.type, cdata = True )

		return _to_xml( 'rtept', attributes = { 'lat': self.latitude, 'lon': self.longitude }, content = content )

class GPXTrackPoint( Location ):

	time = None
	symbol = None
	comment = None

	def __init__( self, latitude, longitude, elevation = None, time = None, symbol = None, comment = None ):
		Location.__init__( self, latitude, longitude, elevation )

		self.time = time
		self.symbol = symbol
		self.comment = comment

	def to_xml( self ):
		content = _to_xml( 'ele', content = self.elevation )
		if self.time:
			content += _to_xml( 'time', content = self.time.strftime( DATE_FORMAT ) )
		content += _to_xml( 'cmt', content = self.comment, cdata = True )
		content += _to_xml( 'sym', content = self.symbol, cdata = True )
		return _to_xml( 'trkpt', { 'lat': self.latitude, 'lon': self.longitude }, content = content )

class GPXTrack:

	name = None
	description = None
	number = None

	track_segments = None

	def __init__( self, name = None, description = None, number = None ):
		self.name = name
		self.description = description
		self.number = number

		self.track_segments = []
	
	def length_2d( self ):
		length = 0
		for track_segment in self.track_segments:
			d = track_segment.length_2d()
			if d == 0:
				return 0
			if not d:
				return None
			length += d
		return length

	def length_3d( self ):
		length = 0
		for track_segment in self.track_segments:
			d = track_segment.length_3d()
			if d == 0:
				return 0
			if not d:
				return None
			length += d
		return length

	def to_xml( self ):
		content = _to_xml( 'name', content = self.name, cdata = True )
		content += _to_xml( 'desc', content = self.description, cdata = True )
		content += _to_xml( 'number', content = self.number )
		for track_segment in self.track_segments:
			content += track_segment.to_xml()

		return _to_xml( 'trk', content = content )

class GPXTrackSegment:

	track_points = None

	def __init__( self ):
		self.track_points = []

	def length_2d( self ):
		return length_2d( self.track_points )

	def length_3d( self ):
		return length_3d( self.track_points )

	def to_xml( self ):
		content = ''
		for track_point in self.track_points:
			content += track_point.to_xml()
		return _to_xml( 'trkseg', content = content )

class GPX:

	time = None
	name = None
	description = None
	author = None
	email = None
	url = None
	urlname = None
	time = None
	keywords = None

	waypoints = []
	routes = []
	tracks = []

	min_latitude = None
	max_latitude = None
	min_longitude = None
	max_longitude = None

	def __init__( self, waypoints = None, routes = None, tracks = None ):
		self.time = None

		if waypoints: self.waypoints = waypoints
		else: self.waypoints = []

		if routes: self.routes = routes
		else: self.routes = []

		if tracks: self.tracks = tracks
		else: self.tracks = []

		self.name = None
		self.description = None
		self.author = None
		self.email = None
		self.url = None
		self.urlname = None
		self.time = None
		self.keywords = None

		self.min_latitude = None
		self.max_latitude = None
		self.min_longitude = None
		self.max_longitude = None

	def length_2d( self ):
		result = 0
		for track in self.tracks:
			length = track.length_2d()
			if length or length == 0:
				result += length
		return result

	def length_3d( self ):
		result = 0
		for track in self.tracks:
			length = track.length_3d()
			if length or length == 0:
				result += length
		return result

	def to_xml( self ):
		content = _to_xml( 'time', content = self.time )
		content += _to_xml( 'name', content = self.name, cdata = True )
		content += _to_xml( 'desc', content = self.description, cdata = True )
		content += _to_xml( 'author', content = self.author, cdata = True )
		content += _to_xml( 'email', content = self.email, cdata = True )
		content += _to_xml( 'url', content = self.url, cdata = True )
		content += _to_xml( 'urlname', content = self.urlname, cdata = True )
		if self.time:
			content = _to_xml( 'time', content = self.time.strftime( DATE_FORMAT ) )
		content += _to_xml( 'keywords', content = self.keywords, cdata = True )

		for waypoint in self.waypoints:
			content += waypoint.to_xml()

		for track in self.tracks:
			content += track.to_xml()

		for route in self.routes:
			content += route.to_xml()

		return _to_xml( 'gpx', attributes = {}, content = content )

class GPXParser:

	xml = None

	valid = None
	error = None

	gpx = None

	def __init__( self, xml_or_file = None ):
		if hasattr( xml_or_file, 'read' ):
			self.xml = xml_or_file.read()
		else:
			self.xml = str( xml_or_file )

		self.valid = False
		self.error = None

		self.gpx = GPX()

	def parse( self ):
		try:
			dom = minidom.parseString( self.xml )
			self._parse_dom( dom )

			return self.gpx
		except Exception, e:
			logging.debug( 'Error in:\n%s\n-----------\n' % self.xml )
			logging.exception( e )
			self.error = str( e )

			return None

	def _parse_dom( self, dom ):
		root_nodes = dom.childNodes

		root_node = None

		logging.debug( 'XML:%s' % self.xml.replace( '\n', ' ' )[ 0 : 100 ] )

		for node in root_nodes:
			if not root_node:
				node_name = node.nodeName
				if node_name == 'gpx':
					root_node = node

		for node in root_node.childNodes:
			node_name = node.nodeName
			if node_name == 'time':
				time_str = self.get_node_data( node )
				self.gpx.time = _parse_time( time_str )
			elif node_name == 'name':
				self.gpx.name = self.get_node_data( node )
			elif node_name == 'desc':
				self.gpx.description = self.get_node_data( node )
			elif node_name == 'author':
				self.gpx.author = self.get_node_data( node )
			elif node_name == 'email':
				self.gpx.email = self.get_node_data( node )
			elif node_name == 'url':
				self.gpx.url = self.get_node_data( node )
			elif node_name == 'urlname':
				self.gpx.urlname = self.get_node_data( node )
			elif node_name == 'keywords':
				self.gpx.keywords = self.get_node_data( node )
			elif node_name == 'bounds':
				self._parse_bounds( node )
			elif node_name == 'wpt':
				self.gpx.waypoints.append( self._parse_waypoint( node ) )
			elif node_name == 'rte':
				self.gpx.routes.append( self._parse_route( node ) )
			elif node_name == 'trk':
				self.gpx.tracks.append( self._parse_track( node ) )
			else:
				#print 'unknown %s' % node
				pass

		self.valid = True

	def _parse_bounds( self, node ):
		if node.attributes.has_key( 'minlat' ):
			self.gpx.min_latitude = _to_number( node.attributes[ 'minlat' ].nodeValue )
		if node.attributes.has_key( 'maxlat' ):
			self.gpx.min_latitude = _to_number( node.attributes[ 'maxlat' ].nodeValue )
		if node.attributes.has_key( 'minlon' ):
			self.gpx.min_longitude = _to_number( node.attributes[ 'minlon' ].nodeValue )
		if node.attributes.has_key( 'maxlon' ):
			self.gpx.min_longitude = _to_number( node.attributes[ 'maxlon' ].nodeValue )

	def _parse_waypoint( self, node ):
		if not node.attributes.has_key( 'lat' ):
			raise Exception( 'Waypoint without latitude' )
		if not node.attributes.has_key( 'lon' ):
			raise Exception( 'Waypoint without longitude' )

		lat = node.attributes[ 'lat' ].nodeValue
		lon = node.attributes[ 'lon' ].nodeValue

		elevation_node = _find_first_node( node, 'ele' )
		elevation = _to_number( self.get_node_data( elevation_node ), 0 )

		time_node = _find_first_node( node, 'time' )
		time_str = self.get_node_data( time_node )
		time = _parse_time( time_str )

		name_node = _find_first_node( node, 'name' )
		name = self.get_node_data( name_node )

		desc_node = _find_first_node( node, 'desc' )
		desc = self.get_node_data( desc_node )

		sym_node = _find_first_node( node, 'sym' )
		sym = self.get_node_data( sym_node )

		type_node = _find_first_node( node, 'type' )
		type = self.get_node_data( type_node )

		return GPXWaypoint( lat, lon, elevation, time, name, desc, sym, type )

	def _parse_route( self, node ):
		name_node = _find_first_node( node, 'name' )
		name = self.get_node_data( name_node )

		description_node = _find_first_node( node, 'desc' )
		description = self.get_node_data( description_node )

		number_node = _find_first_node( node, 'number' )
		number = _to_number( self.get_node_data( number_node ) )

		route = GPXRoute( name, description, number )

		child_nodes = node.childNodes
		for child_node in child_nodes:
			node_name = child_node.nodeName
			if node_name == 'rtept':
				route_point = self._parse_route_point( child_node )
				route.route_points.append( route_point )

		return route

	def _parse_route_point( self, node ):
		if not node.attributes.has_key( 'lat' ):
			raise Exception( 'Waypoint without latitude' )
		if not node.attributes.has_key( 'lon' ):
			raise Exception( 'Waypoint without longitude' )

		lat = node.attributes[ 'lat' ].nodeValue
		lon = node.attributes[ 'lon' ].nodeValue

		elevation_node = _find_first_node( node, 'ele' )
		elevation = _to_number( self.get_node_data( elevation_node ), 0 )

		time_node = _find_first_node( node, 'time' )
		time_str = self.get_node_data( time_node )
		time = _parse_time( time_str )

		name_node = _find_first_node( node, 'name' )
		name = self.get_node_data( name_node )

		desc_node = _find_first_node( node, 'desc' )
		desc = self.get_node_data( desc_node )

		sym_node = _find_first_node( node, 'sym' )
		sym = self.get_node_data( sym_node )

		type_node = _find_first_node( node, 'type' )
		type = self.get_node_data( type_node )

		comment_node = _find_first_node( node, 'cmt' )
		comment = self.get_node_data( comment_node )

		return GPXRoutePoint( lat, lon, elevation, time, name, desc, sym, type, comment )

	def _parse_track( self, node ):
		name_node = _find_first_node( node, 'name' )
		name = self.get_node_data( name_node )

		description_node = _find_first_node( node, 'desc' )
		description = self.get_node_data( description_node )

		number_node = _find_first_node( node, 'number' )
		number = _to_number( self.get_node_data( number_node ) )

		track = GPXTrack( name, description, number )

		child_nodes = node.childNodes
		for child_node in child_nodes:
			if child_node.nodeName == 'trkseg':
				track_segment = self._parse_track_segment( child_node )

				track.track_segments.append( track_segment )

		return track

	def _parse_track_segment( self, node ):
		track_segment = GPXTrackSegment()
		child_nodes = node.childNodes
		n = 0
		for child_node in child_nodes:
			if child_node.nodeName == 'trkpt':
				track_point = self._parse_track_point( child_node )
				track_segment.track_points.append( track_point )
				n += 1

		return track_segment

	def _parse_track_point( self, node ):
		latitude = None
		if node.attributes.has_key( 'lat' ):
			latitude = _to_number( node.attributes[ 'lat' ].nodeValue )

		longitude = None
		if node.attributes.has_key( 'lon' ):
			longitude = _to_number( node.attributes[ 'lon' ].nodeValue )

		time_node = _find_first_node( node, 'time' )
		time = _parse_time( self.get_node_data( time_node ) )

		elevation_node = _find_first_node( node, 'ele' )
		elevation = _to_number( self.get_node_data( elevation_node ) )

		symbol_node = _find_first_node( node, 'sym' )
		symbol = self.get_node_data( symbol_node )

		comment_node = _find_first_node( node, 'cmt' )
		comment = self.get_node_data( comment_node )

		return GPXTrackPoint( latitude, longitude, elevation, time, symbol, comment )

	def is_valid( self ):
		return self.valid

	def get_error( self ):
		return self.error

	def get_gpx( self ):
		return GPX( waypoints = self.waypoints, routes = self.routes, tracks = self.tracks )

	def get_node_data( self, node ):
		if not node:
			return None
		child_nodes = node.childNodes
		if not child_nodes or len( child_nodes ) == 0:
			return None
		return child_nodes[ 0 ].data

if __name__ == '__main__':

	file_name = 'test_files/aaa.gpx'
	#file_name = 'test_files/blue_hills.gpx'
	#file_name = 'test_files/test.gpx'
	file = open( file_name, 'r' )
	gpx_xml = file.read()
	file.close()

	parser = GPXParser( gpx_xml )
	gpx = parser.parse()

	print gpx.to_xml()

	if parser.is_valid():
		print 'TRACKS:'
		for track in gpx.tracks:
			print 'name%s, 2d:%s, 3d:%s' % ( track.name, track.length_2d(), track.length_3d() )
			print '\tTRACK SEGMENTS:'
			for track_segment in track.track_segments:
				print '\t2d:%s, 3d:%s' % ( track_segment.length_2d(), track_segment.length_3d() )

		print 'ROUTES:'
		for route in gpx.routes:
			print route.name
	else:
		print 'error: %s' % parser.get_error()
